﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;

namespace SqlFileLayoutViewer
{
    class PageBitmap
    {
        private int imageWidth = 1000;
        private int imageHeight; // This is computed from the number of pages analyzed
        public int barHeight = 14;
        public int barWidth = 2;
        public int pageTypeHeight = 4;

        public bool highlightFrag = true;
        public bool showPageFreeSpace = true;

        private int fragHeight = 2;
        private HSLColor fragHighlightColor = new HSLColor(6d, 240d, 130d);

        private List<HSLColor> objColorPal;
        private List<HSLColor> pageTypeColorPal;
        private Dictionary<Int64, Int32> objColorAssignments;

        public Bitmap pageAnalysisImage;

        public PageBitmap(DBAnalyzer pa, bool highlightFrag = true, bool showPageFreeSpace = true)
        {

            // Paint a visualization of the page information in pa

            this.highlightFrag = highlightFrag;
            this.showPageFreeSpace = showPageFreeSpace;

            objColorPal = CreateColorPalette();

            // Gray pallet will be used to label page types

            pageTypeColorPal = CreateGrayPalette();

            // Each database object gets assigned a color; this dictionary keeps track of what
            // color index from the palette is assigned to what database object

            objColorAssignments = new Dictionary<Int64, Int32>();

            // The bitmap into which the image of the data page analysis will be painted

            imageHeight = Convert.ToInt32(Math.Ceiling(pa.PageList.Count() / Convert.ToDouble(imageWidth / barWidth)) * barHeight);
            pageAnalysisImage = new Bitmap(imageWidth, imageHeight);

            paintBitmap(pa);

        }

        private static List<HSLColor> CreateGrayPalette()
        {
            // Generate a color lookup table with a variety of grays
            List<HSLColor> GPalette = new List<HSLColor>();
            for (int i = 0; i < 15; i++)
            {
                GPalette.Add(new HSLColor(0d, 0d, i * 20d));
            }
            return GPalette;
        }

        private static List<HSLColor> CreateColorPalette()
        {
            // Generate a color lookup table with a variety of colors
            List<HSLColor> CPalette = new List<HSLColor>();
            CPalette.Add(new HSLColor(0d, 0d, 120d)); // Gray tone for unformatted unused pages is color 0
            CPalette.Add(new HSLColor(0d, 0d, 140d)); // Gray tone for formatted but unused pages is color 1
            CPalette.Add(new HSLColor(0d, 0d, 160d)); // Gray tone for formatted and allocated but unused pages is 2
            CPalette.AddRange(ColorLib.GenColorPal(97, 230, 190));
            return CPalette;
        }

        private void paintBitmap(DBAnalyzer pa)
        {
            int x = 0;
            int y = 0;

            int pageTypeColorIndex = 0;
            int objColorIndex = 0;
            int nextColor = 1;

            foreach (PageInfo pi in pa.PageList)
            {
                // Set color per page type
                switch (pi.PageType)
                {
                    case 1:
                        pageTypeColorIndex = 14;
                        break;
                    case 2:
                        pageTypeColorIndex = 13;
                        break;
                    case 3:
                        pageTypeColorIndex = 12;
                        break;
                    case 4:
                        pageTypeColorIndex = 11;
                        break;

                    /* 5 and 6 don't exist
                    case 5:
                        pageTypeColorIndex = 11;
                        break;
                    case 6:
                        pageTypeColorIndex = 10;
                        break;
                    */

                    case 7:
                        pageTypeColorIndex = 10;
                        break;
                    case 8:
                        pageTypeColorIndex = 9;
                        break;
                    case 9:
                        pageTypeColorIndex = 8;
                        break;
                    case 10:
                        pageTypeColorIndex = 7;
                        break;
                    case 11:
                        pageTypeColorIndex = 6;
                        break;
                    /* 12 does not exist
                    case 12:
                        pageTypeColorIndex = 5;
                        break;
                    */
                    case 13:
                        pageTypeColorIndex = 5;
                        break;
                    case 14:
                        pageTypeColorIndex = 4;
                        break;
                    case 15:
                        pageTypeColorIndex = 3;
                        break;
                    case 16:
                        pageTypeColorIndex = 2;
                        break;
                    case 17:
                        pageTypeColorIndex = 1;
                        break;
                    default:
                        pageTypeColorIndex = 1;
                        break;
                }

                // Set color per db object (if any)
                if (pi.GAMAllocated || pi.SGAMAllocated) {
                    // Color-code allocated pages by type and partition
                    switch (pi.PageType) {
                        case 0:
                            objColorIndex = 0;
                            break;
                        case 1:
                        case 2:
                        case 3:
                        case 4:
                            // Data, Index or LOB page
                            // Added special case 9/19/2013: Some 'abandonded' pages can report being 
                            // GAM allocated but not belong to an object, so objectid = 0. Make these a
                            // gray tone
                            if (pi.ObjectID == 0) {
                                objColorIndex = 2;
                            }
                            else {
                                // Object id is present, so color code by object/partition
                                if (objColorAssignments.TryGetValue(pi.PartitionID, out objColorIndex)) {
                                    // Reuse already assigned color if the dictionary has one for this ptn
                                }
                                else {
                                    // Assign the next color if this is a new ptn
                                    // If we run out of colors cycle back to the first color
                                    // after the initial grays in the color collection
                                    objColorAssignments.Add(pi.PartitionID, nextColor);
                                    objColorIndex = nextColor;
                                    nextColor++;
                                    if (nextColor > objColorPal.Count() - 1) nextColor = 3;
                                }
                            }
                            break;
                        default:
                            objColorIndex = 3;
                            break;
                    }
                }
                else {
                    // Grayscale unallocated pages in two tones depending on if they have a type
                    // (ones with a type were written to at some point in the past
                    // even if not in use now. This is useful to show "page movement"
                    // from one area of a file to another)
                    switch (pi.PageType) {
                        case 0:
                            objColorIndex = 0;
                            break;
                        case 11:
                            // PFS pages are the exception - we want to see those even though DBCC page
                            // does not make them look allocated
                            objColorIndex = 3;
                            break;
                        default:
                            objColorIndex = 1;
                            break;
                    }
                }

                // Paint the small bar representing the current DB page
                int px = 0;
                int py = 0;

                // This is used to paint only part of the bar proportial to space used:

                int fillHeight = 0;
                fillHeight = (int)Math.Ceiling((pi.FreeSpace / 100m) * (barHeight - pageTypeHeight));

                for (int i = 0; i < barWidth; i++) {
                    for (int j = 0; j < barHeight; j++) {

                        // Current pixel within the bar:
                        px = x + i;
                        py = y + j;

                        if (j < pageTypeHeight) {   

                            // Paint the page type color at the top of each bar.
                            // Optionally use part of this area to highlight fragmentation,
                            // if the preference to highlight frag is set and the current page is not
                            // contiguous with the next:

                            if (highlightFrag && j >= pageTypeHeight - fragHeight && DBAnalyzer.IsFrag(pi)) {
                                pageAnalysisImage.SetPixel(px, py, fragHighlightColor);
                            }
                            else {
                                pageAnalysisImage.SetPixel(px, py, pageTypeColorPal[pageTypeColorIndex]);
                            }
                        }
                        else
                        { // In the lower part of the bar paint using the object color, possibly in two sections
                          // to show page free space, if the page free space preference is set
                          // and dithered with a darker version of the colof if the page is not
                          // contiguous with the next page for the same object

                            // For pages that are only partially full show gray for the top portion
                            // when show page free space preference is set

                            if ( showPageFreeSpace && j < pageTypeHeight + fillHeight) {
                                pageAnalysisImage.SetPixel(px, py, objColorPal[2]);
                            }
                            else {

                                // If the page is a fragment boundary, use a darker, dithered color
                                // to represent the used data area
                                if (DBAnalyzer.IsFrag(pi) && pi.PartitionID > 0) {

                                    HSLColor light = new HSLColor(objColorPal[objColorIndex]);
                                    light.Luminosity -= 15;

                                    HSLColor dark = new HSLColor(light);
                                    dark.Luminosity -= 60;

                                    if ((i + j) % 2 == 0) {
                                        pageAnalysisImage.SetPixel(px, py, light);
                                    }
                                    else {
                                        pageAnalysisImage.SetPixel(px, py, dark);
                                    }
                                }
                                else {
                                    // Othewise paint used data area with normal, solid color
                                    // This is happy, defragged, dense data
                                    pageAnalysisImage.SetPixel(px, py, objColorPal[objColorIndex]);
                                }
                            }
                        }
                    }
                }

                // Move to the next bar. If we are at the right edge of the bitmap, 
                // move down to a new row

                x += barWidth;
                if (x + barWidth > pageAnalysisImage.Width)
                {
                    x = 0;
                    y += barHeight;
                }
            }
        }
    }
}
